package com.google.code.guava.beans;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import com.google.code.guava.base.Exceptions;
import com.google.code.guava.reflect.Classes;
import com.google.common.base.Function;
import com.google.common.base.Objects;

/**
 * This is an abstraction over a JavaBeans {@link PropertyDescriptor} (and the
 * underlying field, if available). The generic type is equivalent to
 * {@link PropertyDescriptor#getPropertyType()}.
 * 
 * @param <T>
 */
public class Property<T> extends BaseProperty<T>{

    enum PropertyToName implements Function<PropertyDescriptor, String>{

        INSTANCE{

            public String apply(final PropertyDescriptor input){
                return input.getName();
            }
        }
    }

    /**
     * Does this property have a getter method?
     */
    @Override
    public boolean isReadable(){
        return getter != null;
    }

    /**
     * Does this property have a setter method?
     */
    @Override
    public boolean isWriteable(){
        return setter != null;
    }

    /**
     * Does this property have a backing field?
     * 
     * @see #getField()
     */
    public boolean isBackedByField(){
        return field != null;
    }

    /**
     * Provides a type safe generic view of this property. Usage:
     * 
     * <pre>
     * Property<?> someProperty = // initialize this somehow
     * if(String.class.equals(someProperty.getType())){
     *     someProperty.asType(String.class).setValue(bean, "Hello World");
     * }
     * </pre>
     * 
     * @param <S>
     * @param targetType
     *            the target class
     * @return this property, casted to the supplied generic type
     * @exception IllegalArgumentException
     *                if the supplied type is not compatible with
     *                {@link #getType()}
     */
    @SuppressWarnings("unchecked" /* checked at runtime */)
    public <S> Property<S> asType(final Class<S> targetType){
        checkArgument(targetType.isAssignableFrom(getType())
            || Classes.equivalentWhenAutoBoxing(targetType,
                getType()),
                "Property %s is not compatible with return type %s",
                getName(),
                getType());
        return (Property<S>) this;
    }

    @Override
    public int hashCode(){
        return Objects.hashCode(name, field, getter, setter);
    }

    @Override
    public boolean equals(final Object obj){
        if(obj instanceof Property<?>){
            final Property<?> other = (Property<?>) obj;
            return Objects.equal(name, other.name)
            && Objects.equal(field, other.field)
            && Objects.equal(getter, other.getter)
            && Objects.equal(setter, other.setter);
        }
        return false;
    }

    private final Field field;
    private final Method getter;
    private final String name;
    private final Method setter;
    private final Class<T> type;

    @SuppressWarnings("unchecked")
    Property(final PropertyDescriptor descriptor){
        getter = descriptor.getReadMethod();
        setter = descriptor.getWriteMethod();
        name = descriptor.getName();
        type = (Class<T>) descriptor.getPropertyType();

        final Method reference = getter == null ? setter : getter;
        Field tmpField;
        try{
            tmpField = reference.getDeclaringClass()
            .getDeclaredField(descriptor.getName());
        }catch(final SecurityException e){
            throw Exceptions.rethrow(e);
        }catch(final NoSuchFieldException e){
            // that's OK, there doesn't have to be a field
            tmpField = null;
        }
        field = tmpField;
    }

    /**
     * Return the underlying field (if any).
     * <p/>
     * <b>Caveat:</b> There is no way to determine the correlation between
     * fields and accessors by reflection. By convention, a bean property is
     * usually backed by a field of the same name and type, but this is not
     * required, nor is there any special mechanism to retrieve such a field. In
     * theory, there could be a field that has the correct name and type, but
     * has nothing to do with this property. Sorry, we have no way to find out.
     * 
     * @return the underlying field, or null
     */
    public Field getField(){
        return field;
    };

    /**
     * Get the value of the underlying field. Throws a
     * {@link NullPointerException} if there is no field, so use
     * {@link #isBackedByField()} first.
     * 
     * @param bean
     *            a bean that has this property
     * @see #isBackedByField()
     */
    @SuppressWarnings("unchecked")
    public T getFieldValue(final Object bean){
        try{
            checkNotNull(field, "Field not defined");
            field.setAccessible(true);
            return (T) field.get(bean);
        }catch(final IllegalArgumentException e){
            throw Exceptions.rethrow(e);
        }catch(final IllegalAccessException e){
            throw Exceptions.rethrow(e);
        }
    };

    /**
     * Get the getter method, if available. Call {@link #isReadable()} to see if
     * a getter is available.
     * 
     * @return the getter method, or null
     * @see #isReadable()
     */
    public Method getGetter(){
        return getter;
    };

    /**
     * Return the bean property name. According to the JavaBeans convention,
     * this name will be <code>foo</code> if
     * <ul>
     * <li>{@link #getGetter()} returns a method called either
     * <code>getFoo()</code> or <code>isFoo()</code> (the latter is valid for
     * boolean properties only)
     * <li>and / or {@link #getSetter()} returns a method called
     * <code>setFoo()</code>
     * </ul>
     * This name can never be null or empty, and it will only contain valid java
     * identifier characters as identified by
     * {@link Character#isJavaIdentifierPart(char)}.
     */
    @Override
    public String getName(){
        return name;
    }

    /**
     * Get the setter method, if available. Call {@link #isWriteable()} to see
     * if a setter is available.
     * 
     * @return the setter method, or null
     * @see #isWriteable()
     */
    public Method getSetter(){
        return setter;
    }

    /**
     * Gets the property value by invoking the getter. Throws
     * {@link NullPointerException} if no getter exists, so check
     * {@link #isReadable()} before accessing this.
     */
    @SuppressWarnings("unchecked")
    public T getValueFromGetter(final Object bean){
        try{
            checkNotNull(getter, "Getter not defined");
            return (T) getGetter().invoke(bean);
        }catch(final IllegalArgumentException e){
            throw Exceptions.rethrow(e);
        }catch(final IllegalAccessException e){
            throw Exceptions.rethrow(e);
        }catch(final InvocationTargetException e){
            throw Exceptions.rethrow(e);
        }

    }

    public void setFieldValue(final Object bean, final T value){
        try{
            field.setAccessible(true);
            checkNotNull(field, "Field not defined");
            field.set(bean, value);
        }catch(final IllegalArgumentException e){
            throw Exceptions.rethrow(e);
        }catch(final IllegalAccessException e){
            throw Exceptions.rethrow(e);
        }
    }

    public void setValueWithSetter(final Object bean, final T value){
        try{
            checkNotNull(setter, "Setter not defined");
            setter.invoke(bean, value);
        }catch(final IllegalArgumentException e){
            throw Exceptions.rethrow(e);
        }catch(final IllegalAccessException e){
            throw Exceptions.rethrow(e);
        }catch(final InvocationTargetException e){
            throw Exceptions.rethrow(e);
        }
    };

    @Override
    public Class<T> getType(){
        return type;
    }


}
